﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CategoryAxis.cs" company="OxyPlot">
//   Copyright (c) 2014 OxyPlot contributors
// </copyright>
// <summary>
//   Represents a category axis.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace OxyPlot.Axes
{
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    /// <summary>
    /// Represents a category axis.
    /// </summary>
    /// <remarks>The category axis is using the index of the label collection items as coordinates.
    /// If you have 5 categories in the Labels collection, the categories will be placed at coordinates 0 to 4.
    /// The range of the axis will be from -0.5 to 4.5 (excluding padding).</remarks>
    public class CategoryAxis : LinearAxis
    {
        /// <summary>
        /// The auto-generated labels.
        /// </summary>
        private readonly List<string> autoGeneratedLabels = new List<string>();

        /// <summary>
        /// The labels from the <see cref="ItemsSource" />.
        /// </summary>
        private readonly List<string> itemsSourceLabels = new List<string>();

        /// <summary>
        /// Initializes a new instance of the <see cref="CategoryAxis" /> class.
        /// </summary>
        public CategoryAxis()
        {
            this.TickStyle = TickStyle.Outside;
            this.Position = AxisPosition.Bottom;
            this.MinimumPadding = 0;
            this.MaximumPadding = 0;
            this.MajorStep = 1;
            this.GapWidth = 1;
        }

        /// <summary>
        /// Gets the actual category labels.
        /// </summary>
        public IList<string> ActualLabels
        {
            get
            {
                if (this.ItemsSource != null)
                {
                    return this.itemsSourceLabels;
                }

                if (this.Labels.Count > 0)
                {
                    return this.Labels;
                }

                return this.autoGeneratedLabels;
            }
        }

        /// <summary>
        /// Gets or sets the gap width.
        /// </summary>
        /// <remarks>The default value is 1.0 (100%). The gap width is given as a fraction of the total width/height of the items in a category.</remarks>
        public double GapWidth { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the ticks are centered. If this is <c>false</c>, ticks will be drawn between each category. If this is <c>true</c>, ticks will be drawn in the middle of each category.
        /// </summary>
        public bool IsTickCentered { get; set; }

        /// <summary>
        /// Gets or sets the items source (used to update the Labels collection).
        /// </summary>
        /// <value>The items source.</value>
        public IEnumerable ItemsSource { get; set; }

        /// <summary>
        /// Gets or sets the data field for the labels.
        /// </summary>
        public string LabelField { get; set; }

        /// <summary>
        /// Gets the list of category labels.
        /// </summary>
        public List<string> Labels { get; } = new List<string>();

        /// <inheritdoc/>
        public override void GetTickValues(
            out IList<double> majorLabelValues, out IList<double> majorTickValues, out IList<double> minorTickValues)
        {
            base.GetTickValues(out majorLabelValues, out majorTickValues, out minorTickValues);
            minorTickValues.Clear();

            if (!this.IsTickCentered)
            {
                const double epsilon = 1e-3;

                // Subtract 0.5 from the label values to get the tick values.
                // Add one extra tick at the end.
                var mv = new List<double>(majorLabelValues.Count + 1);
                mv.AddRange(majorLabelValues.Select(v => v - 0.5).Where(v => v > this.ClipMinimum - epsilon));

                if (mv.Count > 0)
                {
                    var lastTick = mv[mv.Count - 1] + this.MajorStep;
                    if (lastTick < this.ClipMaximum + epsilon)
                    {
                        mv.Add(lastTick);
                    }
                }

                majorTickValues = mv;
            }
        }

        /// <inheritdoc/>
        public override object GetValue(double x)
        {
            return this.FormatValue(x);
        }

        /// <inheritdoc/>
        internal override void UpdateActualMaxMin()
        {
            // Update the DataMinimum/DataMaximum from the number of categories
            this.Include(-0.5);

            var actualLabels = this.ActualLabels;

            if (actualLabels.Count > 0)
            {
                this.Include((actualLabels.Count - 1) + 0.5);
            }
            else
            {
                this.Include(0.5);
            }

            base.UpdateActualMaxMin();

            this.MinorStep = 1;
        }

        /// <summary>
        /// Updates the category labels.
        /// </summary>
        /// <param name="numberOfCategories">The number of categories.</param>
        protected internal void UpdateLabels(int numberOfCategories)
        {
            if (this.ItemsSource != null)
            {
                this.itemsSourceLabels.Clear();
                this.itemsSourceLabels.AddRange(this.ItemsSource.Format(this.LabelField, this.StringFormat, this.ActualCulture));
                return;
            }

            if (this.Labels.Count == 0)
            {
                if (this.autoGeneratedLabels.Count == numberOfCategories)
                {
                    return;
                }

                this.autoGeneratedLabels.Clear();
                this.autoGeneratedLabels.AddRange(Enumerable.Range(1, numberOfCategories).Select(number => number.ToString(CultureInfo.InvariantCulture)));
            }
        }

        /// <inheritdoc/>
        protected override string FormatValueOverride(double x)
        {
            var index = (int)x;
            var actualLabels = this.ActualLabels;
            if (index >= 0 && index < actualLabels.Count)
            {
                return actualLabels[index];
            }

            return null;
        }
    }
}
